package com.virjar.vscrawler.core.config;

import com.virjar.dungproxy.client.ningclient.concurrent.NamedThreadFactory;
import com.virjar.vscrawler.core.VSCrawlerContext;
import com.virjar.vscrawler.core.event.support.AutoEventRegistry;
import com.virjar.vscrawler.core.event.systemevent.CrawlerEndEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

/**
 * 文件系统目录和文件监控服务
 *
 * @author 杨尚川
 *
 * @author LJ
 * 设计思路：
 * 通过什么路径下的改变，执行什么操作(抽象类)
 * 1. 创建一个单例： 实现事件处理的函数-处理路径下的某一个事件(抽象类)、监听的事件[]数组
 * 2. 初始化一个线程池、监听事件
 * 3. 处理函数丢给线程池去处理
 * 4. 监听路径时会将wathcSerivce，与监听事件相关联
 * 5. 线程池里面的线程处理watchSevice监听的path改变时的返回值WatchKey；
 *
 */
public class DirectoryWatcher {
    private static final Logger LOGGER = LoggerFactory.getLogger(DirectoryWatcher.class);

    /**
     * 该类对象就是操作系统原生的文件系统监控器，我们知道os自己的文件系统监控器可以监控文件系统上所有的文件的变化
     * 这种监控是无需遍历，无需比较的，是一种基于信号收发的监控，
     * JDK1.7版本之后才有的NIO.2
     * 具体网站可以参考(https://www.ibm.com/developerworks/cn/java/j-nio2-2/index.html)
     */
    private WatchService watchService = null;


    /**
     * 路径下的改变，会导致监听路径的WatchService会返回相应的WatchKey. 然而，需要注意的
     * 是不存在在方法调用用来找出WatchKey注册到哪个目录。所以我么需要记录WatchKey与路径的关系
     */
    private final Map<WatchKey, Path> directories = new HashMap<>();

    /**
     * 双重检查锁定来实现懒汉式单例，需要在静态成员变量之前增加修饰符volatile,
     * 被volatile修饰的成员变量可以确保多线程都能够正确处理，但是volatile关键字会
     * 屏蔽java虚拟机所做的一切代码优化，可能会导致系统运行效率降低
     */
    private volatile static ExecutorService EXECUTOR_SERVICE = null;

    /**
     * 1. 一看kind就知道该类一定是一个枚举类，里面定义类文件变化的各种枚举类型；
     * 2. 这些枚举变量都定义在StandardWatchEventKinds中，主要有三个：
     *    ENTRY_CREATE: 创建
     *    ENTRY_DELETE: 删除
     *    ENTRY_MODIFY: 修改
     */
    private WatchEvent.Kind<?>[] events;


    /**
     *
     * @param watcherCallback
     * @param events
     * @return
     */
    public static DirectoryWatcher getDirectoryWatcher(final WatcherCallback watcherCallback,
                                                       WatchEvent.Kind<?>... events) {
        return new DirectoryWatcher(watcherCallback, events);

    }


    /**
     * 私有函数
     * @param watcherCallback
     *          回调
     * @param events
     *          事件
     */
    private DirectoryWatcher(final WatcherCallback watcherCallback, WatchEvent.Kind<?>... events) {
        //AutoEventRegistry.getInstance().registerObserver(this);
        try {
            if (events.length == 0) {
                throw new RuntimeException(
                        "必须至少指定一个监控的事件，如：StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE");
            }

            /**
             * 创建单例子：双重检查锁定
             */
            if (null == EXECUTOR_SERVICE){
                synchronized (DirectoryWatcher.class) {
                    if (EXECUTOR_SERVICE == null) {
                        // 创建的线程类似于CachedThreadPool
                        // Executors.newCachedThreadPool()
                        EXECUTOR_SERVICE = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>(), new NamedThreadFactory("watch-service"));
                    }
                }
            }

            this.events = new WatchEvent.Kind<?>[events.length];
            int i = 0;
            for (WatchEvent.Kind<?> event : events) {
                this.events[i++] = event;
                LOGGER.info("注册事件：" + event.name());
            }

            /**
             *  监控器其实就是一个后台线程，在后台监控文件变化发出信号；
             *  语句只是刚刚初始化的线程，连就绪状态都没有进入，只是初始化而已
             */
            watchService = FileSystems.getDefault().newWatchService();


            EXECUTOR_SERVICE.submit(new Runnable() {
                @Override
                public void run() {
                    watch(watcherCallback);
                }

            });
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    if (!EXECUTOR_SERVICE.isShutdown()) {
                        //线程池本身就是deamo的
                        EXECUTOR_SERVICE.shutdown();
                    }
                }
            });
        } catch (IOException ex) {
            LOGGER.error("构造文件系统监控服务失败：", ex);
            throw new RuntimeException(ex);
        }
    }

    /**
     * 监控指定目录，不监控子目录
     *
     * @param path
     */
    public void watchDirectory(String path) {
        watchDirectory(Paths.get(path));
    }

    /**
     * 监控指定目录，不监控子目录
     *
     * @param path
     */
    public void watchDirectory(Path path) {
        registerDirectory(path);
    }

    /**
     * 监控指定的目录及其所有子目录
     *
     * @param path
     */
    public void watchDirectoryTree(String path) {
        watchDirectoryTree(Paths.get(path));
    }

    /**
     * 监控指定的目录及其所有子目录
     *
     * @param path
     */
    public void watchDirectoryTree(Path path) {
        registerDirectoryTree(path);
    }

    /**
     * 关闭监控线程
     */
    public void close() {
        EXECUTOR_SERVICE.shutdown();
    }

    /**
     * 监控事件分发器
     *
     * @param watcherCallback 事件回调
     */
    private void watch(WatcherCallback watcherCallback) {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                /**
                 * 1. 即监控键，说的明确一点就是该文件节点所绑定的监控器的监控器的监控信息池，即文件节点的监控池，简称监控池
                 * 2. register方法返回的就是节点的监控池
                 */
                final WatchKey key = watchService.take();  // 它将无限期等待，直到可以返回WatchKey
                if (key == null) {
                    continue;
                }
                for (WatchEvent<?> watchEvent : key.pollEvents()) {
                    final WatchEvent.Kind<?> kind = watchEvent.kind();
                    // 忽略无效事件
                    if (kind == StandardWatchEventKinds.OVERFLOW) {
                        continue;
                    }
                    final WatchEvent<Path> watchEventPath = (WatchEvent<Path>) watchEvent;
                    // path是相对路径（相对于监控目录）
                    final Path contextPath = watchEventPath.context();
                    LOGGER.info("contextPath:" + contextPath);
                    // 获取监控目录
                    final Path directoryPath = directories.get(key);
                    LOGGER.info("directoryPath:" + directoryPath);
                    // 得到绝对路径
                    final Path absolutePath = directoryPath.resolve(contextPath);
                    LOGGER.info("absolutePath:" + absolutePath);
                    LOGGER.info("kind:" + kind);
                    // 判断事件类别
                    switch (kind.name()) {
                        case "ENTRY_CREATE":
                            if (Files.isDirectory(absolutePath, LinkOption.NOFOLLOW_LINKS)) {
                                LOGGER.info("新增目录：" + absolutePath);
                                // 为新增的目录及其所有子目录注册监控事件
                                registerDirectoryTree(absolutePath);
                            } else {
                                LOGGER.info("新增文件：" + absolutePath);
                            }
                            break;
                        case "ENTRY_DELETE":
                            LOGGER.info("删除：" + absolutePath);
                            break;
                        case "ENTRY_MODIFY":
                            LOGGER.info("修改：" + absolutePath);
                            break;
                    }
                    // 某一个事件
                    watcherCallback.execute(kind, absolutePath.toAbsolutePath().toString());
                }
                boolean valid = key.reset();
                if (!valid) {
                    if (directories.get(key) != null) {
                        LOGGER.info("停止监控目录：" + directories.get(key));
                        directories.remove(key);
                    }
                }
            }
        } catch (InterruptedException ex) {
            LOGGER.info("监控目录线程退出");
        } finally {
            try {
                watchService.close();
                LOGGER.info("关闭监控目录服务");
            } catch (IOException ex) {
                LOGGER.error("关闭监控目录服务出错", ex);
            }
        }
    }

    /**
     * 为指定目录及其所有子目录注册监控事件
     *
     * @param path 目录
     */
    private void registerDirectoryTree(Path path) {
        try {
            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    registerDirectory(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException ex) {
            LOGGER.error("监控目录失败：" + path.toAbsolutePath(), ex);
        }
    }

    /**
     * 为指定目录注册监控事件
     *
     * @param path
     */
    private void registerDirectory(Path path) {
        try {
            LOGGER.info("监控目录:" + path);
            /**
             *  1. 其意义很明确，就是将指定的监控器注册给Path对象所代表的文件节点，而events则指定
             *  了监控器监控哪些类型的文件变化
             *  2. 该方法同时也会让监控器线程就绪并运行，该方法调用完后，监控器就彻底开始监控了
             */
            WatchKey key = path.register(watchService, events);
            directories.put(key, path);
        } catch (IOException ex) {
            LOGGER.error("监控目录失败：" + path.toAbsolutePath(), ex);
        }
    }


    /**
     * 事件回调
     */
    public interface WatcherCallback {
        void execute(WatchEvent.Kind<?> kind, String path);
    }
}